/*
 * PositionTracer.java
 *
 * Created on 1 juin 2006, 10:06
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */
package courselog.gpsutils;

import courselog.CourseLogAppForm;
import ewe.io.File;
import ewe.io.FileOutputStream;
import ewe.io.IOException;
import ewe.ui.Event;
import ewe.ui.EventListener;
import ewe.ui.ReportException;
import ewe.util.Vector;
//import ewe.util.EventDispatcher;
/**
 * Traces the position obtained by a SerialGPSConnection into a logging file.
 * Also provides calculations of the current duration, cumulative distance,
 * average speed, average heading.
 * About PositionTracer status :
 * STATUS_OFF : The tracer is off, no recording.
 * STATUS_RECORDING : The tracer is recording ReceiverEvents and generating trace.
 * STATUS_HOLD : The tracer is on hold. If it restarts, a new Receiver may be activated,
 *               anyway, old records will be kept. <B>Useful when the recorder is activated
 *               from time to time.</B>
 * STATUS_PAUSED : The tracer is not recording. Moves are not considered part of the trace.
 *                 <B>Useful when you are stopping your treck and want to make moves that are
 *                 not interesting to record.</B>
 *
 *
 *
 * @author C. Rigal
 */
public class PositionTracer implements EventListener {

    /**
     * Connected GPS receiver.
     */
    public SerialGPSConnection myGPS;
    /**
     * Event Listener when state of the Tracer changes.
     * Use of a Dispatcher would have been prefered but did not work with
     * windows 2005 Pocket (as the same code worked with Java under WinXP).
     */
    protected Vector vDisplayer = new Vector(2);
    /**
     * Logging file. Traces of points are placed here.
     */
    public FileOutputStream fLog = null;
    /**
     * Static buffer where logged data is written (256 chars).
     */
    protected byte[] buff = new byte[256];
    /**
     * Status when PositionTracer is not active (==0).
     */
    public final static int STATUS_OFF = 0;
    /**
     * Status when PositionTracer is not recording (==1).
     */
    public final static int STATUS_RECORDING = 1;
    /**
     * Status when PositionTracer is on hold (==2).
     */
    public final static int STATUS_HOLD = 2;
    /**
     * Status when PositionTracer is paused (==3).
     */
    public final static int STATUS_PAUSED = 3;
    /**
     * PositionTracer status, see constants.
     * Use setStatus to set it. Read it publicly.
     */
    public int myStatus = STATUS_OFF;
    /** Option indicating that the receiver state has changed  */
    public static final int OPT_STATUSCHANGED = 1;
    /** Option indicating that the track has changed  */
    public static final int OPT_TRACECHANGED = 2;
    /**
     * Record of the current.
     */
    public TrackRecord myTrack;
    /**
     * Current point of the trace.
     */
    public TrackPoint myCurrent = new TrackPoint();
    /**
     * Previous point, spaced at least by <I>minDistance</I> from current point.
     */
    public TrackPoint lastSignificantPoint = null;
    /**
     * Previous reference altitude. To compare with <I>minAltitudeDifference</I>
     * to count variations from current point.
     */
    public double lastReferenceAltitude = 0.0d;
    /** Last upwards value taken for reference. */
    protected double lastUpwards = 0.0d;
    /** Last downwards value taken for reference. */
    protected double lastDownwards = 0.0d;
    /** Flag indicating that next update of trace should consider that it is out of
     * pause mode. */
    protected boolean isOutOfPause = false;
    /**
     * Flag to force next point to be logged, whatever the state.
     */
    protected boolean isNextLogForced = false;
    /**
     * Minimum distance between logged points. As GPS involves measurement
     * errors, only points separated by this distance are logged in order to
     * reduce points in case you don't move.
     */
    public double minDistance = 10.0d;
    /**
     * Minimum altitude to count difference points. As GPS involves measurement
     * errors, only altitudes separated by this distance are counted in the
     * vertical moves in order to reduce false moves.
     * Initial value 5.0 m.
     */
    public double minAltitudeDifference = 10.0d;
    /**
     * Name of the logging file.
     */
    public String fileLogName;

    // CONSTRUCTORS
    /**
     * Creates a new instance of PositionTracer with a logging file
     * @param sGPS GPS receiver associated to this trace.
     * @param newStatus The status in case this tracer is created after another one.
     * @param logName Name of the file where the log should be placed. If <I>null</I>, no file is written.
     */
    public PositionTracer(SerialGPSConnection sGPS, int newStatus, String logName) {
        myGPS = sGPS;
        myTrack = new TrackRecord();
        fileLogName = logName;
        if (newStatus != STATUS_OFF) {
            File fL = new File(fileLogName);
            if (fL.canRead()) {
                myTrack = TrackRecord.readLogFile(fL, myTrack,
                        TrackRecord.OPTION_ENDTRACK);
                int nbP = myTrack.vPoints.size();
                if (nbP > 0) {
                    GeoPoint lastPt = (GeoPoint) myTrack.vPoints.elementAt(nbP - 1);
                    if (lastPt instanceof TrackPoint) {
                        myCurrent.copyFrom(lastPt);
                        lastSignificantPoint = new TrackPoint();
                        lastSignificantPoint.copyFrom(myCurrent);
                    }
                }
            }
        }
        myStatus = newStatus;
        myGPS.setListener(this);
    }

    // ACCESSORS
    /**
     * Set the log file name.
     * This closes a previous log if any.
     * Call this with newName==null to close the log file.
     * @param newName New log file name.
     */
    public void setLogFileName(String newName) {
        if (fLog != null) {
            try {
                fLog.close();
            } catch (IOException ex) {
                new ReportException(ex).execute();
            }
            fLog = null;
        }
        fileLogName = newName;
        myTrack.vPoints.setSize(0);
    }

    /**
     * Change the status of the tracer.
     * @param newStatus Should be one of STATUS_XXX values.
     */
    public void setStatus(int newStatus) {
        if (newStatus == STATUS_RECORDING) {
            if (myStatus == STATUS_OFF) {
                myTrack.vPoints.setSize(0);
                lastSignificantPoint = null;
                isNextLogForced = true;
            } else if (myStatus == STATUS_PAUSED) {
                isOutOfPause = true;
            }
        // If HOLD nothing to do.
        } else {
            if (myStatus == STATUS_RECORDING) {
                // If status set to OFF, log next point
                // If status set to Hold, force log of next point
                // If status set to PAUSE, force log too.
                isNextLogForced = true;
            }
        }
        this.myStatus = newStatus;
    }

    /**
     * The status of the GPS receiver.
     * @return one value of SerialGPSConnection.STATUS_xxx.
     */
    public int getGPSStatus() {
        return myGPS.status;
    }

    // CLASS METHODS
    /**
     * Read initial attributes of this PositionTracer from a buffer content.
     * Should be able to read a buffer formatted by <B>formatInBuffer</B> and
     * restore the tracer status content.
     * <PRE>
     * Significant fields are :
     * - Date
     * - Time (UTC)
     * - Latitude
     * - Longitude
     * - Altitude
     * - Total distance
     * - Total time
     * - Total up
     * - Total down
     * </PRE>
     * A small discripancy in the usage of this hold mode : last reference
     * altitude is the one from the last logged point, as, in continuous mode,
     * it is the one from the last altitude used in reference.
     *
     *
     *
     * @param newStatus New value of status for the tracer.
     * @param log Buffer from which we read in values.
     */
    public void initFromStateAndLog(int newStatus, String log) {
        double[] vDbl = new double[13];
        int[] vInt = new int[2];
        char[] vCh = new char[3];

        // Parse content
        String sequenceLog = "IVVVVCCCVVVVVVVVVI";
        Converters.parseCSVSentence(sequenceLog, log.getBytes(), 0, log.length() - 1, vDbl, vInt, vCh);
        // Put values in fields
        this.lastSignificantPoint = new TrackPoint();
        this.lastSignificantPoint.date = vInt[0]; // Date first
        this.lastSignificantPoint.time = vDbl[0]; // Time UTC
        this.lastSignificantPoint.lat = vDbl[1]; // Signed latitude
        this.lastSignificantPoint.lon = vDbl[2]; // Signed longitude
        this.lastSignificantPoint.altSeaLevel = vDbl[3]; // Altitude
        lastReferenceAltitude = vDbl[3];
        this.lastSignificantPoint.speed = vDbl[6];
        this.lastSignificantPoint.heading = vDbl[7];
        this.lastSignificantPoint.avgSpeed = vDbl[8];
        this.lastSignificantPoint.avgHeading = vDbl[9];
        this.lastSignificantPoint.lengthHere = vDbl[10];
        this.lastSignificantPoint.upwardsHere = vDbl[11];
        this.lastSignificantPoint.downwardsHere = vDbl[12];
        this.lastSignificantPoint.durationHere = (double) vInt[1];
        // Update computed values
        this.lastSignificantPoint.computeTime();
        this.lastSignificantPoint.computeLatLonToUTMRef();
        this.lastSignificantPoint.computeCartesian();

        // Set Status
        this.myStatus = newStatus;
    }

    /**
     * Activated when a ReceiverEvent is generated by the GPS.
     * My call its own listener with an event signalling the trace has changed.
     * @param rEv Event from the GPS.
     */
    public void onEvent(Event rEv) {
        //CourseLog.logger.setLog("\nPositionTracer#onEvent- type "+rEv.type);
        try {
            if (rEv instanceof ReceiverEvent) {
                int opt = OPT_STATUSCHANGED; // As a minimum, the receiver is updated.
                if (rEv.type == ReceiverEvent.NEWPOINT) {
                    // There is a new point but the track is not necessarly updated.
                    if (updateTrace((MeasuredPoint) rEv.target)) {
                        opt |= OPT_TRACECHANGED;
                    }
                } else {
//                    CourseLogAppForm.dbgL.addToLog("PositionTracer.onEvent: GPS status changed=="+myGPS.status+"\n");
                }
                for (int idxD = vDisplayer.size() - 1; idxD >= 0; idxD--) {
                    XPositionDisplayer xpd = (XPositionDisplayer) vDisplayer.elementAt(idxD);
                    if (xpd.isActive()) {
                        xpd.listenerUpdated(opt, this);
                    }
                }
            }
        } catch (NullPointerException ex) {
            CourseLogAppForm.dbgL.addExceptionToLog(ex);
        } catch (IndexOutOfBoundsException ex) {
            CourseLogAppForm.dbgL.addExceptionToLog(ex);
        } catch (ClassCastException ex) {
            CourseLogAppForm.dbgL.addExceptionToLog(ex);
        } catch (RuntimeException ex) {
            CourseLogAppForm.dbgL.addExceptionToLog(ex);
//        }finally {
//            (new MessageBox("Exception","Exception in GPS event loop,"+
//                    " see logfile.txt for details\n",MessageBox.MBOK)).execute();
        }
    }

    /**
     * Add a position displayer to this tracer.
     * @param newDisp New displayer to add.
     */
    public void addDisplayer(XPositionDisplayer newDisp) {
        vDisplayer.add(newDisp);
    }

    /**
     * Remove a position displayer to this tracer.
     * @param newDisp Displayer to remove.
     */
    public void removeDisplayer(XPositionDisplayer newDisp) {
        vDisplayer.remove(newDisp);
    }

    /**
     * Called when a new event is received and tracer is not off, in order to
     * update the tracer state.
     * If the point is distant enough from the last point, the trace log is
     * updated.
     * @param pt New point to take into account.
     * @return True if the trace state has changed, false otherwise.
     */
    public boolean updateTrace(MeasuredPoint pt) {
        boolean updatedTrace = false;

        if (myGPS.status != SerialGPSConnection.STATUS_OK) {
            return false;
        }

        pt.computeTime();
        pt.computeLatLonToUTMRef();
        pt.computeCartesian();

        myCurrent.copyFrom(pt); // Copy measured point attributes

        if (lastSignificantPoint == null) { // initialise last significant point
            lastSignificantPoint = new TrackPoint();
            lastSignificantPoint.copyFrom(myCurrent);
            lastReferenceAltitude = myCurrent.altSeaLevel;
            lastDownwards = 0.0d;
            lastUpwards = 0.0d;
            lastSignificantPoint.durationHere = 0.0d;
            lastSignificantPoint.upwardsHere = 0.0d;
            lastSignificantPoint.downwardsHere = 0.0d;
            lastSignificantPoint.lengthHere = 0.0d;
        }
        // Update TrackPoint specific fields
        double tmpTime = pt.timeTo(lastSignificantPoint);
        double distanceToPrevious = pt.distance(lastSignificantPoint);
        double diffAlt = pt.altSeaLevel - lastReferenceAltitude;

        if (isOutOfPause) {
            isOutOfPause = false;
            myCurrent.durationHere = lastSignificantPoint.durationHere;
            myCurrent.lengthHere = lastSignificantPoint.lengthHere;
            myCurrent.upwardsHere = lastSignificantPoint.upwardsHere;
            myCurrent.downwardsHere = lastSignificantPoint.downwardsHere;
            lastReferenceAltitude = pt.altSeaLevel;
            myCurrent.avgSpeed = 0;
            myCurrent.avgHeading = lastSignificantPoint.heading;
        } else {
            myCurrent.durationHere = tmpTime +
                    lastSignificantPoint.durationHere;
            myCurrent.lengthHere = distanceToPrevious +
                    lastSignificantPoint.lengthHere;
            if (diffAlt > 0) {
                myCurrent.upwardsHere = diffAlt + lastUpwards;
                myCurrent.downwardsHere = lastDownwards;
            } else {
                myCurrent.downwardsHere = -diffAlt + lastDownwards;
                myCurrent.upwardsHere = lastUpwards;
            }
            if (tmpTime == 0) {
                myCurrent.avgSpeed = 0.0d;
            } else {
                myCurrent.avgSpeed = 3.6d * distanceToPrevious / tmpTime;
            }
            myCurrent.avgHeading = lastSignificantPoint.headingTo(pt);
        }

        //CourseLog.logger.addToLog("\n PositionTracer#updateTrace- cP.time : "+cP.time);
        if ((distanceToPrevious > minDistance) || (isNextLogForced)) {
            // Move is significant while recording or log is forced
            isNextLogForced = false;
            // Altitude changes are counted separately from the distance
            // let's use their last reference.
            if (Math.abs(diffAlt) >= minAltitudeDifference) {
                lastReferenceAltitude = pt.altSeaLevel;
                lastUpwards = myCurrent.upwardsHere;
                lastDownwards = myCurrent.downwardsHere;
            }

            if (myStatus == STATUS_RECORDING) {
                updatedTrace = true;
                TrackPoint newP = new TrackPoint();
                newP.copyFrom(myCurrent);
                logAPoint(newP); // put the values of the point in the log file
                myTrack.vPoints.addElement(newP);
            }
            lastSignificantPoint.copyFrom(myCurrent); // update last point
        }

        return updatedTrace;
    }

    /**
     * Writes a point in the logging file.
     * @param pt Point to put in the log.
     */
    public void logAPoint(TrackPoint pt) {
        try {
            if (fLog == null) {
                if (fileLogName != null) {
                    fLog = new FileOutputStream(fileLogName, true);
                }
            }

            if (fLog != null) {
                if (pt.myX == MeasuredPoint.dummyCoord) {
                    pt.computeTime();
                    pt.computeLatLonToUTMRef();
                    pt.computeCartesian();
                }
                int lg = pt.formatInBuffer(buff);

                try {
                    fLog.write(buff, 0, lg);
                    if (myStatus == STATUS_OFF) {
                        fLog.close();
                        fLog = null;
                    }
                } catch (IOException ex) {
                // Nothing to do...
                }
            }
        } catch (IOException ex) {
            fLog = null;
        }

    }
}
